package org.samoji.sji.profTokenizer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;

import org.samoji.sji.profTokenizer.Token.Type;

public class Tokenizer {
    private static final int EOS = -1;
    private static final int CR = '\r';
    private static final int LF = '\n';
    
    private final BufferedReader in;
    private final StringBuilder builder = new StringBuilder();
    private int ch;
    private boolean stepBack;
    private Token token;
    private Token prevToken;

    public Tokenizer(Reader in) {
        this.in = new BufferedReader(in);
    }

    public Token prev() {
        if (stepBack)
            throw new TokenizerException("already in step back mode");
        stepBack = true;
        return prevToken;
    }
    
    public Token token() {
        return stepBack ? prevToken : token;
    }
    
    public Token next() throws IOException {
        if (stepBack) {
            stepBack = false;
            return token;
        }
        prevToken = token;
        skipWhites();
        if ((token = id()) != null)
            return token;
        if ((token = num()) != null)
            return token;
        if ((token = operatorOrDelimiter()) != null)
            return token;
        return token = new Token(Type.Unknown);
    }

    /*
     * Upon entry the stream position is just before the operator or delimiter.
     * Upon exit the stream position is just after the operator or delimiter.
     */
    private Token operatorOrDelimiter() throws IOException {
        markAndRead(1);
        switch (ch) {
        case EOS: return new Token(Type.Eos);
        case '+': return plusOperator();
        case '-': return minusOperator();
        case '*': return timesOperator();
        case '/': return divOperator();
        case '%': return modOperator();
        case '=': return new Token(Type.Becomes);
        case '(': return new Token(Type.OpenParen);
        case ')': return new Token(Type.CloseParen);
        case ';': return new Token(Type.Semicolon);
        }
        return resetAndToken(null);
    }

    private Token divOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.DivBecomes);
        }
        return resetAndToken(new Token(Type.Div));
    }

    private Token modOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.ModBecomes);
        }
        return resetAndToken(new Token(Type.Mod));
    }

    private Token timesOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.TimesBecomes);
        case '*': return timesTimesOperator();
        }
        return resetAndToken(new Token(Type.Times));
    }

    private Token timesTimesOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.PowBecomes);
        }
        return resetAndToken(new Token(Type.Pow));
    }

    private Token minusOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.MinusBecomes);
        }
        return resetAndToken(new Token(Type.Minus));
    }

    private Token plusOperator() throws IOException {
        markAndRead(1);
        switch (ch) {
        case '=': return new Token(Type.PlusBecomes);
        }
        return resetAndToken(new Token(Type.Plus));
    }

    /*
     * Upon entry the stream position is just before the id.
     * Upon exit the stream position is just after the id.
     */
    private Token id() throws IOException {
        markAndRead(1);
        if (!isIdentStart())
            return resetAndToken(null);
        do
            appendMarkAndRead();
        while (isIdentPart());
        return resetAndToken(new Token(Type.Id, null, toStringAndReset()));
    }

    /*
     * Upon entry the stream position is just before the num.
     * Upon exit the stream position is just after the num.
     */
    private Token num() throws IOException {
        markAndRead(2);
        if (isDigit()) {
            digits();
            if (isFractionIndicator()) {
                appendMarkAndRead();
                if (isDigit())
                    digits();
            }
        } else if (isFractionIndicator()) {
            append();
            read();
            if (!isDigit())
                return resetAndToken(null);
            digits();
        } else
            return resetAndToken(null);
        if (isExponentIndicator())
            exponent();
        return resetAndToken(new Token(Type.Num, new BigDecimal(toStringAndReset()), null));
    }

    private void exponent() throws IOException {
        reset();
        markAndRead(3);
        int length = builder.length();
        append();
        read();
        if (isSign()) {
            append();
            read();
        }
        if (isDigit())
            digits();
        else
            builder.setLength(length);
    }

    private void digits() throws IOException {
        do
            appendMarkAndRead();
        while (isDigit());
    }

    /*
     * Upon entry stream position is before the optional whites.
     * Upon exit stream position is just after them.
     */
    private void skipWhites() throws IOException {
        for (;;) {
            markAndRead(1);
            if (ch == '/') {
                reset();
                markAndRead(2);
                read();
                if (ch == '/')
                    // in.readLine();
                	skipLineComment();
                else if (ch == '*')
                    skipNestingComment();
                else
                    break;
            } else if (!isWhite())
                break;
        }
        reset();
    }

    private void skipNestingComment() throws IOException {
        for (;;) {
            read();
            if (ch == EOS)
                throw new TokenizerException("unclosed nesting comment");
            if (ch == '*') {
                read();
                if (ch == '/')
                    return;
                continue;
            }
            if (ch == '/') {
                read();
                if (ch == '*')
                    skipNestingComment();
                else
                    continue;
            }
        }
    }

    private void skipLineComment() throws IOException {
        do
            read();
        while (!isEol());
    }

    private void markAndRead(int readAheadLimit) throws IOException {
        in.mark(readAheadLimit);
        read();
    }

    private void read() throws IOException {
        ch = in.read();
    }

    private void reset() throws IOException {
        in.reset();
    }

    private void append() {
        builder.append((char) ch);
    }

    private void appendMarkAndRead() throws IOException {
        append();
        markAndRead(1);
    }

    private Token resetAndToken(Token tok) throws IOException {
        reset();
        return tok;
    }

    private boolean isEol() {
    	return ch == CR || ch == LF || ch == EOS;
    }

    private boolean isDigit() {
        return Character.isDigit(ch);
    }

    private boolean isWhite() {
        return Character.isWhitespace(ch);
    }
    
    private boolean isSign() {
        return ch == '-' || ch == '+';
    }

    private boolean isExponentIndicator() {
        return ch == 'e' || ch == 'E';
    }

    private boolean isFractionIndicator() {
        return ch == '.';
    }

    private boolean isIdentPart() {
        return Character.isJavaIdentifierPart(ch);
    }

    private boolean isIdentStart() {
        return Character.isJavaIdentifierStart(ch);
    }

    private String toStringAndReset() {
        String s = builder.toString();
        builder.setLength(0);
        return s;
    }
    
}
